/*
 * Copyright (c) 2007 Compl Yue Still, All Rights Reserved.
 */

package hbi.wire.java.guest;

import hbi.java.util.CharSetConstants;
import hbi.task.TaskAgent;
import hbi.wire.ProtocolError;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class GuestEnd
{

    private final OutputStream out;

    public GuestEnd(OutputStream out)
    {
        this.out = out;
    }

    void writeByte(int b) throws IOException
    {
        out.write(b);
    }

    void writeShort(short n) throws IOException
    {
        out.write(n >> 8);
        out.write(n);
    }

    void writeInt(int n) throws IOException
    {
        out.write(n >> 24);
        out.write(n >> 16);
        out.write(n >> 8);
        out.write(n);
    }

    void writeLong(long n) throws IOException
    {
        out.write((int) (n >> 56));
        out.write((int) (n >> 48));
        out.write((int) (n >> 40));
        out.write((int) (n >> 32));
        out.write((int) (n >> 24));
        out.write((int) (n >> 16));
        out.write((int) (n >> 8));
        out.write((int) (n));
    }

    void writeDate(Date d) throws IOException
    {
        writeLong(d.getTime());
    }

    void writeDouble(double n) throws IOException
    {
        writeLong(Double.doubleToLongBits(n));
    }

    void writeByteArray(byte[] bytes) throws IOException
    {
        int len = bytes.length;
        if (len > Short.MAX_VALUE)
            throw new ProtocolError("Too long byte array.");
        writeShort((short) len);
        out.write(bytes);
    }

    void writeUTF(String s) throws IOException
    {
        writeByteArray(s.getBytes(CharSetConstants.UTF8));
    }

    // assume already properly synchronized from outside

    private final Map<Class<?>, ForecastedType> types = // 
    new HashMap<Class<?>, ForecastedType>();

    private ForecastedType forecastType(
        Class<? extends TaskAgent<?, ?>> agentClass) throws IOException
    {
        ForecastedType t = types.get(agentClass);
        if (t == null)
        {
            writeByte('t');
            writeUTF(agentClass.getCanonicalName());
            t = new ForecastedType(agentClass, types.size(), this);
            types.put(agentClass, t);
        }
        return t;
    }

    private ForecastedType agentType;

    public void startAgent(Class<? extends TaskAgent<?, ?>> agentClass)
        throws IOException
    {
        agentType = forecastType(agentClass);
        writeByte('a');
        writeInt(agentType.index);
    }

    public void scriptAgent(Method method, Object... args) throws IOException
    {
        short methodIndex = agentType.getMethodIndex(method);
        writeByte('c');
        writeShort(methodIndex);
        for (Object arg : args)
            writeObject(arg);
        writeByte('z');
    }

    public void finishAgent() throws IOException
    {
        agentType = null;
        writeByte('z');
        out.flush();
    }

    private void writeObject(Object o) throws IOException
    {
        if (o == null)
        {
            writeByte('N');
        }
        else if (o instanceof Boolean)
        {
            writeByte(((Boolean) o) ? 'T' : 'F');
        }
        else if (o instanceof Integer)
        {
            writeByte('I');
            writeInt((Integer) o);
        }
        else if (o instanceof Long)
        {
            writeByte('L');
            writeLong((Long) o);
        }
        else if (o instanceof Double)
        {
            writeByte('D');
            writeDouble((Double) o);
        }
        else if (o instanceof String)
        {
            writeByte('S');
            writeUTF((String) o);
        }
        else if (o instanceof byte[])
        {
            writeByte('B');
            writeByteArray((byte[]) o);
        }
        else if (o instanceof Date)
        {
            writeByte('d');
            writeDate((Date) o);
        }
        else if (o.getClass().isArray())
        {
            Class<?> elementType = o.getClass().getComponentType();
            if (elementType == Object.class)
            // untyped array
            {
                writeByte(']');
                writeUntypedArray(o);
            }
            else
            // typed array
            {
                int dimension = 1;
                writeByte('[');
                while (elementType.isArray())
                {
                    Class<?> componentType = elementType.getComponentType();
                    if (componentType == Byte.TYPE) break;
                    dimension++;
                    writeByte('[');
                    elementType = componentType;
                }

                if (elementType == Boolean.TYPE)
                {
                    writeByte('T');
                    writeTypedArray(o, dimension, new ElementWriter()
                    {
                        public void writeValue(Object o) throws IOException
                        {
                            writeByte(((Boolean) o) ? 'T' : 'F');
                        }
                    });
                }
                else if (elementType == Integer.TYPE)
                {
                    writeByte('I');
                    writeTypedArray(o, dimension, new ElementWriter()
                    {
                        public void writeValue(Object o) throws IOException
                        {
                            writeInt((Integer) o);
                        }
                    });
                }
                else if (elementType == Long.TYPE)
                {
                    writeByte('L');
                    writeTypedArray(o, dimension, new ElementWriter()
                    {
                        public void writeValue(Object o) throws IOException
                        {
                            writeLong((Long) o);
                        }
                    });
                }
                else if (elementType == Double.TYPE)
                {
                    writeByte('D');
                    writeTypedArray(o, dimension, new ElementWriter()
                    {
                        public void writeValue(Object o) throws IOException
                        {
                            writeDouble((Double) o);
                        }
                    });
                }
                else if (elementType == String.class)
                {
                    writeByte('S');
                    writeTypedArray(o, dimension, new ElementWriter()
                    {
                        public void writeValue(Object o) throws IOException
                        {
                            writeUTF((String) o);
                        }
                    });
                }
                else if (elementType.isArray()) // byte[]
                {
                    assert elementType.getComponentType() == Byte.TYPE;
                    writeByte('B');
                    writeTypedArray(o, dimension, new ElementWriter()
                    {
                        public void writeValue(Object o) throws IOException
                        {
                            writeByteArray((byte[]) o);
                        }
                    });
                }
                else if (elementType == Date.class)
                {
                    writeByte('d');
                    writeTypedArray(o, dimension, new ElementWriter()
                    {
                        public void writeValue(Object o) throws IOException
                        {
                            writeDate((Date) o);
                        }
                    });
                }
                else
                {
                    throw new ProtocolError("Unsupported array element type: "
                        + elementType.getCanonicalName());
                }
            }
        }
        else
        {
            throw new ProtocolError("Bad object type: "
                + o.getClass().getCanonicalName());
        }
    }

    private static interface ElementWriter
    {
        void writeValue(Object o) throws IOException;
    }

    private void writeTypedArray(Object a, int dimension, ElementWriter writer)
        throws IOException
    {
        if (a == null)
        {
            // treat missing element as zero length array
            writeInt(0);
            return;
        }

        int len = Array.getLength(a);
        writeInt(len);
        if (len < 1) return;

        if (dimension > 1)
        {
            dimension--;
            for (int i = 0; i < len; i++)
                writeTypedArray(Array.get(a, i), dimension, writer);
        }
        else
        {
            for (int i = 0; i < len; i++)
                writer.writeValue(Array.get(a, i));
        }
    }

    private void writeUntypedArray(Object a) throws IOException
    {
        int len = Array.getLength(a);
        writeInt(len);
        if (len < 1) return;

        for (int i = 0; i < len; i++)
            writeObject(Array.get(a, i));
    }

}
